Skip to content

jit, stdlib: PR 1187 review follow-ups; cpython_tests: per-host baseline overlay - #1196

Merged
youknowone merged 3 commits into
mainfrom
agent/stdlib-foundations
Aug 13, 2026
Merged

jit, stdlib: PR 1187 review follow-ups; cpython_tests: per-host baseline overlay#1196
youknowone merged 3 commits into
mainfrom
agent/stdlib-foundations

Conversation

@youknowone

Copy link
Copy Markdown
Owner

Follow-ups to the review findings on #1187, plus the per-host baseline the
CPython suite needs to gate on more than one machine.

Review findings

Each was reproduced against the built binary first; the probe output is below.

Module-scope LOAD_NAME builtins fold specialised against a dict it could not
read.
The fold declined only when module_dict_cell_slot_direct found the
name in the frame's live globals, but that call also answers None for a dict
it cannot read at all — a plain dict, or a module dict that ran
switch_to_object_strategy. On any dict other than the namespace operand its
None proved nothing, while the fold guarded the operand's version and the
residual it replaced resolved the live dict, so a name present there would read
the builtin instead of the global. guard_current_frame_globals_identity does
not close the gap: it constrains the frame's globals, not the dict the residual
resolves, and the two differ when frame.pycode != w_code_ptr. Declines now
unless the two are the same dict.

BLAKE2 reported the wrong argument error. The salt/person/key size checks
ran in _blake2_new, after the app-level wrapper had range-checked fanout and
depth. lib_pypy/_blake2/__init__.py sets salt and person before the tree
parameters and the key after them; the checks moved to those positions and
measure Py_buffer.len rather than len(), so a memoryview over a wider
itemsize still counts bytes.

before after CPython 3.14 / pypy3
blake2b(salt=b'x'*17, fanout=256) fanout must be between 0 and 255 maximum salt length is 16 bytes maximum salt length is 16 bytes
blake2b(person=b'x'*17, fanout=256) fanout must be between 0 and 255 maximum person length is 16 bytes maximum person length is 16 bytes
blake2b(salt=b'x'*17, depth=256) depth must be between 1 and 255 maximum salt length is 16 bytes maximum salt length is 16 bytes

All six probe cases now match both reference interpreters.

_PyModule_ClearDict's two name passes were ordering-inert. Rebinding a
name to None frees nothing without refcounting, so a finalizer released by
the private-name pass read its module's public globals as None where CPython
shows them alive. A sweep now runs between the passes — once for the whole
walk, not once per module, which measured 905ms of teardown for
import unittest, re, argparse and timed test_regrtest out. A further
collection runs after the walk releases its module roots: that is the only
point a value stored under a non-string key becomes unreachable, and without it
its __del__ never ran.

probe before after CPython 3.14
__del__ under a non-string module key not run NONSTR DEL RAN NONSTR DEL RAN
public global seen by a private-name finalizer None public-value-alive public-value-alive

Teardown for import unittest, re, argparse goes 38.7ms -> 48.7ms for the two
added collections.

Two further suggestions were declined: rejecting oversized BLAKE2 buffers
before copying is already the effect of measuring nbytes app-level, and the
ruff cleanup of bench/synth/load_name_builtin_cell_fold.py would churn a file
whose module-scope len rebinding is the fixture's subject and whose jit-stats
baselines are recorded against it, for a linter this repository does not run.

Per-host baseline

run.py consults baseline.<sys.platform>-<machine>.json ahead of
baseline.json, and --update-baseline splits what it records: a verdict the
shared file has never seen is written there, a verdict that differs goes to the
host overlay, and a host that comes back into agreement drops its overlay entry
again. No host is privileged — whichever records first sets the shared answer.
The architecture is part of the key because the dynasm backend emits different
machine code on each. This retires CPYTHON_SUITE_BASELINE_HOST, which skipped
the check.py stage anywhere but darwin-arm64.

Gates

  • pyre/check.py --backend dynasm --synthetic-only — ALL PASSED, 408/408
  • cargo fmt --all -- --check — clean
  • LLBC re-extracted on a quiet tree (rc=0, 0 window writes) before the gate run

🤖 Generated with Claude Code

`module.__init__` read its name argument through `w_str_get_value`, which
panics on a lone surrogate, so `types.ModuleType('\udcff')` aborted the
process.  The import machinery reaches it whenever a filename was decoded with
surrogateescape: `test.test_import.test_unencodable_filename` imports
`TESTFN_UNENCODABLE`, which on Linux is the surrogateescape of byte 0xff.  The
module never ran on darwin, where the kernel rejects such a filename outright
(EILSEQ), so the crash showed only on the Linux CPython suite leg.

Store `Module.name` as `Wtf8Buf` rather than `String` -- the same buffer type
`W_UnicodeObject.value` already uses -- and read the argument with
`w_str_get_wtf8`.  `w_module_new*` keep taking `&str`, so their callers are
unchanged.  `release_frees_nothing` answers `false` for a name that is not
valid UTF-8, since such a name cannot key the `&str` `sys.modules` lookup.

Add `tools/ubuntu24-arm64-repro`, an Apple `container` image that reproduces
Linux-only failures natively on Apple silicon rather than through Rosetta.

Assisted-by: Claude
`run.py` now consults `baseline.<sys.platform>-<machine>.json` ahead of
`baseline.json`, and `--update-baseline` splits what it records between the
two: a verdict the shared file has never seen is written there, a verdict
that differs from the shared one goes to the host overlay, and a host that
comes back into agreement drops its overlay entry.

The architecture is part of the key because the dynasm backend emits
different machine code on each one.

Drops `CPYTHON_SUITE_BASELINE_HOST` from `check.py`, which skipped the
cpython-suite stage anywhere other than darwin-arm64.

Assisted-by: Claude
The module-scope LOAD_NAME builtins fold declined only when
`module_dict_cell_slot_direct` found the name in the frame's live globals.
That call also answers `None` for a dict it cannot read -- a plain dict, or a
module dict that switched to the object strategy -- so on any dict other than
the namespace operand its `None` proved nothing, while the fold guarded the
operand's version and the residual it replaced read the live dict. Decline
unless the two are the same dict.

BLAKE2 rejected an oversized salt or person in `_blake2_new`, which runs
after the app-level wrapper has range-checked fanout and depth, so
`blake2b(salt=b'x' * 17, fanout=256)` reported the fanout.
`lib_pypy/_blake2/__init__.py` sets salt and person before the tree
parameters and the key after them; validate in that order, measuring
`Py_buffer.len` rather than `len()` so a memoryview over a wider itemsize
still counts bytes.

`_PyModule_ClearDict`'s two name passes were ordering-inert: rebinding a name
to `None` frees nothing without refcounting, so `_obj.__del__` read its
module's public globals as `None`. Sweep between the passes, once for the
whole walk rather than once per module. Add a collection after the walk
releases its module roots, which is the only point a value stored under a
non-string key becomes unreachable.

Assisted-by: Claude
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@youknowone, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 12 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f8a11ad5-0501-4348-907e-d274788c4436

📥 Commits

Reviewing files that changed from the base of the PR and between d953ddc and 26f6272.

📒 Files selected for processing (10)
  • .github/workflows/pyre-ci.yml
  • pyre/check.py
  • pyre/cpython_tests/run.py
  • pyre/pyre-interpreter/src/module/_blake2/_blake2_app.py
  • pyre/pyre-interpreter/src/typedef.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
  • pyre/pyre-object/src/module.rs
  • pyre/pyrex/src/lib.rs
  • tools/ubuntu24-arm64-repro/Dockerfile
  • tools/ubuntu24-arm64-repro/README.md

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 26f6272025

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread pyre/pyrex/src/lib.rs
Comment on lines +1308 to +1309
clear_pass(ShutdownClearPass::PrivateNames);
collect_and_run_finalizers(ec_ptr);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve per-module teardown ordering

When a private-name finalizer in a newer module reads a private global from an older module, this whole-walk private pass clears the older module's global before running any finalizers. CPython's newest-first module teardown runs that finalizer while the older module is still intact, so a value such as older._secret should remain available rather than becoming None; run the private pass and collection with equivalent per-module ordering instead of batching every module together.

AGENTS.md reference: AGENTS.md:L231-L233

Useful? React with 👍 / 👎.

Comment thread pyre/cpython_tests/run.py
Comment on lines +478 to +482
for source in (overlay, baseline):
entry = source.get("modules", {}).get(module)
if entry is None:
continue
status = entry.get(backend) or entry.get("dynasm")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prefer exact backend entries across baseline layers

When a host overlay has only a dynasm override but the shared baseline has an explicit cranelift verdict, a cranelift run returns the overlay's dynasm fallback before consulting the shared cranelift entry. This can deselect a shared-PASS cranelift module or gate it against the wrong status; resolve exact backend entries across overlay and shared baseline first, and only then apply the dynasm fallback.

Useful? React with 👍 / 👎.

# Salt and person are rejected before the tree parameters and the
# key after them, the order lib_pypy/_blake2 sets each field in.
# `blake2b(salt=b'x' * 17, fanout=256)` reports the salt.
if _buffer_size(salt) > cls.SALT_SIZE:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate key buffers before reporting salt length errors

When key is not buffer-compatible and salt is oversized, this new early salt check raises the salt ValueError, whereas CPython converts the earlier key argument first and raises its bytes-like-object TypeError. Before this change _blake2_new likewise called read_hash_buffer(key) before inspecting salt, so the commit introduces an observable error-order regression; acquire or otherwise validate the key buffer before these salt/person size checks.

Useful? React with 👍 / 👎.

Inside the container:

```bash
scripts/install-charon.sh

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Invoke the Charon installer that exists

The first documented in-container command fails immediately because the repository contains scripts/install-charon.py, not scripts/install-charon.sh (confirmed with a repository-wide file search), and the image does not create a shell wrapper. Use python3 scripts/install-charon.py so the new reproduction procedure is executable.

AGENTS.md reference: AGENTS.md:L62-L65

Useful? React with 👍 / 👎.

@github-actions

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 26f6272).
Updated: 2026-08-13T09:37:21.845Z

Files in the reviewed diff
.github/workflows/pyre-ci.yml
pyre/check.py
pyre/cpython_tests/run.py
pyre/pyre-interpreter/src/module/_blake2/_blake2_app.py
pyre/pyre-interpreter/src/typedef.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
pyre/pyre-object/src/module.rs
pyre/pyrex/src/lib.rs
tools/ubuntu24-arm64-repro/Dockerfile
tools/ubuntu24-arm64-repro/README.md

1. Regressions to PyPy parity introduced by this patch

  • pyre/pyrex/src/lib.rs:1308-1311 ↔ pypy/interpreter/baseobjspace.py:481-502: the new collection between private- and public-name clearing can run __del__ while public globals remain live. PyPy’s ObjSpace.finish() only runs shutdown hooks; it does not perform this module-dictionary clearing/finalizer interleaving. This makes teardown observably less PyPy-like than main’s single post-walk collection.

2. Other mismatches introduced by this patch

None.

3. Pre-existing mismatches (already present before this patch)

  • pyre/pyrex/src/lib.rs:1353-1376 ↔ pypy/interpreter/baseobjspace.py:481-502: pyre explicitly overwrites __main__ globals, detaches sys.modules, and clears module dictionaries during shutdown, whereas PyPy’s shown finish() performs thread/atexit/stream/module-shutdown handling only. This teardown model was already present in upstream/main.

4. Structural adaptations

  • pyre/pyre-interpreter/src/module/_blake2/_blake2_app.py:118-125,145-149 ↔ lib_pypy/_blake2/__init__.py:42-53,99-103: pyre measures buffer arguments with memoryview(...).nbytes, while PyPy uses len(...). This is a CPython 3.14 Argument-Clinic-compatible byte-count adaptation; it correctly handles multi-byte buffer element types.

  • pyre/pyre-object/src/module.rs:24-27,87 ↔ pypy/interpreter/module.py:18-25,85-92: pyre stores a module name as raw WTF-8 rather than PyPy’s managed w_name object. This Rust representation change preserves lone-surrogate module names that String cannot represent; pyre/pyre-interpreter/src/typedef.rs:2836-2839 applies that representation during module.__init__.

@youknowone
youknowone merged commit c0508da into main Aug 13, 2026
20 of 22 checks passed
@youknowone
youknowone deleted the agent/stdlib-foundations branch August 13, 2026 11:11
youknowone added a commit that referenced this pull request Aug 13, 2026
`#1196` changed `Module.name` to a `Wtf8Buf` box and routed
`module.__init__` through `w_str_get_wtf8`, so a name carrying a lone
surrogate no longer panics. Nothing pins that: the two existing accessor
tests only pass an ASCII name, which a `String` box would have held just as
well. Round-trip a name with a lone surrogate through
`w_module_set_name`/`w_module_get_name` and assert it has no `&str` view.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 13, 2026
`#1196` changed `Module.name` to a `Wtf8Buf` box and routed
`module.__init__` through `w_str_get_wtf8`, so a name carrying a lone
surrogate no longer panics. Nothing pins that: the two existing accessor
tests only pass an ASCII name, which a `String` box would have held just as
well. Round-trip a name with a lone surrogate through
`w_module_set_name`/`w_module_get_name` and assert it has no `&str` view.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 14, 2026
`pyre/check.py (windows-latest)` fails 18 rows across 10 fixtures — 9 on each
native backend — where ubuntu-24.04 and macos-latest both pass. Every failing
row is a jit-stats difference; no output snapshot mismatches, so the fixtures
still compute the same results there.

Values transcribed from the windows job of run 31724482401 (main
b0f34c0). Transcription is exact rather than sampled: check.py states that
"the recorded surface and the gated surface are the same set", so a FAIL line
enumerates every counter that differs and each unnamed counter equals the
shared baseline. Each file was cross-checked against the `(observed
loops_compiled=N bridges_compiled=M)` parenthetical the same line prints.

The three runners were read back before adding these, as the overlay comment
requires: at that sha ubuntu reports these rows green (its own failures are
cranelift/str_fstring and wasm/exception_traceback_loop_forms) and macos-latest
is `success` for the whole job.

The divergence appeared with the #1189 squash, but the branch alone does not
produce it: that PR's own last windows run, at head 22ac8c9, failed only
str_fstring on both backends. Its CI merged into d953ddc, while the squash
landed on that plus #1184, #1196 and #1174; main at df365f9 carries those
three without the branch and also lacks these rows. So it is an interaction
between the two sides, and which pair is responsible is not established here.

One caveat for whoever maintains these: inline_chain_depth_typeflip's windows
observation already moved once, 3843 -> 3798, between the squash and
b0f34c0. The other eight fixtures reported identical numbers across both
runs.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 14, 2026
* gc: root every SRE group selector before slicing

* gc: preserve GIL across sandbox heap dumps

* gc: end action borrow before yielding GIL

* gc: make async ticker signal-safe

* gc: root the process signal action

* gc: root the pairwise iteration state across space.next

`next`'s `itertools.pairwise` arm held `self` and `w_prev` in raw Rust locals
across two `space.next` calls. A minor collection inside either call forwards
the object but not the local, so the field stores and the returned tuple could
name pre-collection addresses.

The arm now claims four shadow-stack slots — self, iterator, w_prev, w_next —
before the first call and reloads each from its slot. The indices are fixed
rather than derived from how many roots the taken arm happened to push, so a
slot means the same thing on both paths; the two slots that start without a
value hold null, which the root walkers already read as "no root".

`interp_itertools` gains the field accessors that arm reads and writes through.
The setter runs the write barrier, because `W_Pairwise` is allocated old-gen
and an iterator may yield a nursery object.

The `W_Pairwise` unit test now asserts the GC descriptor's pointer offsets
cover `w_iterator` and `w_prev`, not just the object size.

Assisted-by: Claude

* bench: record the win32 runner jitstats overlays windows-latest reports

`pyre/check.py (windows-latest)` fails 18 rows across 10 fixtures — 9 on each
native backend — where ubuntu-24.04 and macos-latest both pass. Every failing
row is a jit-stats difference; no output snapshot mismatches, so the fixtures
still compute the same results there.

Values transcribed from the windows job of run 31724482401 (main
b0f34c0). Transcription is exact rather than sampled: check.py states that
"the recorded surface and the gated surface are the same set", so a FAIL line
enumerates every counter that differs and each unnamed counter equals the
shared baseline. Each file was cross-checked against the `(observed
loops_compiled=N bridges_compiled=M)` parenthetical the same line prints.

The three runners were read back before adding these, as the overlay comment
requires: at that sha ubuntu reports these rows green (its own failures are
cranelift/str_fstring and wasm/exception_traceback_loop_forms) and macos-latest
is `success` for the whole job.

The divergence appeared with the #1189 squash, but the branch alone does not
produce it: that PR's own last windows run, at head 22ac8c9, failed only
str_fstring on both backends. Its CI merged into d953ddc, while the squash
landed on that plus #1184, #1196 and #1174; main at df365f9 carries those
three without the branch and also lacks these rows. So it is an interaction
between the two sides, and which pair is responsible is not established here.

One caveat for whoever maintains these: inline_chain_depth_typeflip's windows
observation already moved once, 3843 -> 3798, between the squash and
b0f34c0. The other eight fixtures reported identical numbers across both
runs.

Assisted-by: Claude

* Revert "bench: record the win32 runner jitstats overlays windows-latest reports"

An overlay records what a runner observes; it does not change what the runner
observes. The 18 files pinned the windows-latest numbers for those rows so the
gate would stop reporting them, leaving the divergence itself in place.

The pre-existing `str_fstring.cranelift.win32.github-actions.jitstats` is not
part of this and stays.

`pyre/check.py (windows-latest)` therefore still reports the 18 rows.

Assisted-by: Claude
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant